﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.DataSourcesGDB;
using ESRI.ArcGIS.Geometry;
using EsriSystem = ESRI.ArcGIS.esriSystem;
using BesAsm.Framework.ModelResults;
using BesAsm.Emgaats.Core;

namespace BesAsm.Emgaats.ModelResults
{
  public class EmgaatsResultsProvider : IModelResultsProvider
  {
    private IModelResultsHost _host;
    private const string APPLICATION_NAME = "EMGAATS";
    private const string VENDOR = "City of Portland";
    private const string FILE_EXTENSION_FILTER = "Emgaats Model File|*.mdb";
    private const string SUPPORTED_VERSIONS = "3.0";

    private const string MODEL_RESULTS_TEMPLATE = "ModelResultsTemplate.mdb";    

    public EmgaatsResultsProvider()
    {
    }

    #region IModelResultsProvider properties
    public string ApplicationName
    {
      get
      {
        return APPLICATION_NAME;
      }
    }

    public string Vendor
    {
      get
      {
        return VENDOR;
      }

    }

    public string FileExtensionFilter
    {
      get
      {
        return FILE_EXTENSION_FILTER;
      }

    }

    public string SupportedVersions
    {
      get
      {
        return SUPPORTED_VERSIONS;
      }

    }

    public IModelResultsHost ModelResultsHost
    {
      get
      {
        return _host;
      }
      set
      {
        _host = value;
      }
    }
    #endregion
    
    public bool SaveResults(ModelResultsSet modelResults, string outputFile, string modelFile)
    {
      if (System.IO.File.Exists(outputFile))
        throw new ArgumentException("Error writing results: File '" + outputFile + "' already exists");

      try
      {
        CreateResultsPgdb(outputFile);

        IWorkspaceFactory workspaceFactory = new AccessWorkspaceFactoryClass();
        IFeatureWorkspace resultsFws = (IFeatureWorkspace)workspaceFactory.OpenFromFile(outputFile, 0);
        IFeatureWorkspace modelFws = (IFeatureWorkspace)workspaceFactory.OpenFromFile(modelFile, 0);

        WriteModelResultsTable(modelResults, resultsFws);
        WriteLinkResultsTable(modelResults, resultsFws, modelFws);
        WriteNodeResultsTable(modelResults, resultsFws, modelFws);
        WriteDscResultsTable(modelResults, resultsFws, modelFws);

      }
      catch (Exception ex)
      {
        if (System.IO.File.Exists(outputFile))
          try
          {
            System.IO.File.Delete(outputFile);
          }
          catch (Exception)
          {
          }
        return false;
      }

      return true;
    }

    private void CreateResultsPgdb(string file)
    {
      string templatePgdb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), MODEL_RESULTS_TEMPLATE);

      System.IO.FileStream outStream = null;
      try
      {
        outStream = new System.IO.FileStream(file, System.IO.FileMode.Create);
        byte[] data = Properties.Resources.ModelResultsTemplate;
        outStream.Write(data, 0, data.Length);
        outStream.Flush();
      }
      finally
      {
        if (outStream != null)
          outStream.Close();
      }
    }

    private void WriteModelResultsTable(ModelResultsSet modelResults, IFeatureWorkspace resultsFws)
    {
      IRowBuffer rowBuffer = null;

      try
      {
        ITable modelResultsTable = (ITable)resultsFws.OpenTable("MODEL_RESULTS");
        ICursor cursor = modelResultsTable.Insert(true);
        rowBuffer = modelResultsTable.CreateRowBuffer();

        rowBuffer.set_Value(rowBuffer.Fields.FindField("results_file"), modelResults.ResultsFile);
        rowBuffer.set_Value(rowBuffer.Fields.FindField("model_engine"), modelResults.ModelEngine);
        rowBuffer.set_Value(rowBuffer.Fields.FindField("name"), modelResults.Name);
        rowBuffer.set_Value(rowBuffer.Fields.FindField("description"), modelResults.Description);
        rowBuffer.set_Value(rowBuffer.Fields.FindField("simulation_date"), modelResults.SimulationDateTime);
        rowBuffer.set_Value(rowBuffer.Fields.FindField("append_date"), System.DateTime.Now);

        object o = cursor.InsertRow(rowBuffer);

        cursor.Flush();
      }
      finally
      {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(rowBuffer);
      }
    }
    private void WriteLinkResultsTable(ModelResultsSet modelResults, IFeatureWorkspace resultsFws, IFeatureWorkspace modelFws)
    {
      IFeatureBuffer featureBuffer = null;

      try
      {
        IFeatureClass linkFC = (IFeatureClass)modelFws.OpenFeatureClass("MODEL_LINKS");

        Dictionary<string, IGeometry> geometries = GetGeometryDictionary(linkFC, "link_name");
        Dictionary<string, object> ids = GetFieldDictionary(linkFC, "link_name", "link_id");

        IFeatureClass linkResultsFC = (IFeatureClass)resultsFws.OpenFeatureClass("LINK_RESULTS");
        IFeatureCursor featureCursor = linkResultsFC.Insert(true);
        featureBuffer = linkResultsFC.CreateFeatureBuffer();

        foreach (LinkResult lr in modelResults.LinkResults.Values)
        {
          featureBuffer = linkResultsFC.CreateFeatureBuffer();

          featureBuffer.set_Value(featureBuffer.Fields.FindField("modeled_link_name"), lr.LinkName);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_flow"), lr.MaxFlow);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_velocity"), lr.MaxVelocity);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_us_water_elev"), lr.MaxUsWaterElev);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_ds_water_elev"), lr.MaxDsWaterElev);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_qq_ratio"), lr.MaxQqRatio);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_dd_ratio"), lr.MaxDdRatio);

          //TODO: Simulated links have "M" in front (for master data). This is not persisted in a database anywhere,
          //so the first character of the simulated element name is removed. Reconsider this design.
          string formattedLinkName = lr.LinkName.Remove(0, 1);
          if (geometries.ContainsKey(formattedLinkName))
          {
            int id = (int)ids[formattedLinkName];

            featureBuffer.set_Value(featureBuffer.Fields.FindField("link_id"), id);
            featureBuffer.Shape = geometries[formattedLinkName];
          }
          else
          {
            featureBuffer.Shape = new PolylineClass();
          }

          object o = featureCursor.InsertFeature(featureBuffer);
          featureCursor.Flush();
        }
      }
      finally
      {
        if (featureBuffer != null)
          System.Runtime.InteropServices.Marshal.ReleaseComObject(featureBuffer);
      }
    }
    private void WriteNodeResultsTable(ModelResultsSet modelResults, IFeatureWorkspace resultsFws, IFeatureWorkspace modelFws)
    {
      IFeatureBuffer featureBuffer = null;

      try
      {
        IFeatureClass nodeFC = (IFeatureClass)modelFws.OpenFeatureClass("MODEL_NODES");

        Dictionary<string, IGeometry> geometries = GetGeometryDictionary(nodeFC, "node_name");
        Dictionary<string, object> ids = GetFieldDictionary(nodeFC, "node_name", "node_id");

        IFeatureClass nodeResultsFC = (IFeatureClass)resultsFws.OpenFeatureClass("NODE_RESULTS");
        IFeatureCursor featureCursor = nodeResultsFC.Insert(true);
        featureBuffer = nodeResultsFC.CreateFeatureBuffer();

        foreach (NodeResult nr in modelResults.NodeResults.Values)
        {
          featureBuffer = nodeResultsFC.CreateFeatureBuffer();

          featureBuffer.set_Value(featureBuffer.Fields.FindField("modeled_node_name"), nr.NodeName);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_water_elev"), nr.MaxWaterElev);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_surcharge"), nr.MaxSurcharge);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("min_freeboard"), nr.MinFreeboard);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("surcharged_time"), nr.SurchargedTime);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("flooded_time"), nr.FloodedTime);

          string nodeName = nr.NodeName;
          if (geometries.ContainsKey(nodeName))
          {
            int id = (int)ids[nodeName];

            featureBuffer.set_Value(featureBuffer.Fields.FindField("node_id"), id);
            featureBuffer.Shape = geometries[nodeName];
          }
          else
          {
            featureBuffer.Shape = new PointClass();
          }

          object o = featureCursor.InsertFeature(featureBuffer);
          featureCursor.Flush();
        }
      }
      finally
      {
        if (featureBuffer != null)
          System.Runtime.InteropServices.Marshal.ReleaseComObject(featureBuffer);
      }
    }
    private void WriteDscResultsTable(ModelResultsSet modelResults, IFeatureWorkspace resultsFws, IFeatureWorkspace modelFws)
    {
      IFeatureBuffer featureBuffer = null;

      try
      {
        IFeatureClass dscFC = (IFeatureClass)modelFws.OpenFeatureClass("MODEL_DSCS");
        IFeatureClass linkFC = (IFeatureClass)modelFws.OpenFeatureClass("MODEL_LINKS");

        Dictionary<int, DscInfo> dscInfos = GetDscInfos(dscFC, linkFC);

        IFeatureClass dscResultsFC = (IFeatureClass)resultsFws.OpenFeatureClass("DSC_RESULTS");
        IFeatureCursor featureCursor = dscResultsFC.Insert(true);
        featureBuffer = dscResultsFC.CreateFeatureBuffer();

        foreach (DscInfo dscInfo in dscInfos.Values)
        {
          if (dscInfo.ToLinkId == null)
            continue;

          //TODO: Simulated links have "M" in front (for master data). This is not persisted in a database anywhere,
          //so the first character of the simulated element name is removed. Reconsider this design.
          string formattedLinkName = "M" + dscInfo.ToLinkName;
          if (!modelResults.LinkResults.ContainsKey(formattedLinkName))
            continue;

          double maxHgl, maxDeltaHgl, maxSurcharge;
          LinkResult lr = modelResults.LinkResults[formattedLinkName];

          maxHgl = lr.MaxUsWaterElev - (dscInfo.ConnectionFractionFromUsNode * (lr.MaxUsWaterElev - lr.MaxDsWaterElev));
          maxDeltaHgl = dscInfo.FloodReferenceElevation - maxHgl;
          //TODO: Calculate maxSurcharge 
          maxSurcharge = 0;

          featureBuffer = dscResultsFC.CreateFeatureBuffer();

          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_hgl"), maxHgl);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_delta_hgl"), maxDeltaHgl);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("max_surcharge"), maxSurcharge);
          featureBuffer.set_Value(featureBuffer.Fields.FindField("dsc_id"), dscInfo.DscId);
          featureBuffer.Shape = dscInfo.Geometry;

          object o = featureCursor.InsertFeature(featureBuffer);
          featureCursor.Flush();
        }
      }
      finally
      {
        if (featureBuffer != null)
          System.Runtime.InteropServices.Marshal.ReleaseComObject(featureBuffer);
      }
    }

    private Dictionary<int, DscInfo> GetDscInfos(IFeatureClass dscFC, IFeatureClass linkFC)
    {
      Dictionary<int, DscInfo> dscInfos = new Dictionary<int, DscInfo>();
      Dictionary<int, string> linkNames = new Dictionary<int, string>();

      IFeatureCursor cursor = linkFC.Search(null, true);
      IFeature feature = cursor.NextFeature();

      while (feature != null)
      {
        int linkId = (int)feature.get_Value(feature.Fields.FindField("link_id"));
        string linkName = (string)feature.get_Value(feature.Fields.FindField("link_name"));

        linkNames.Add(linkId, linkName);
        feature = cursor.NextFeature();
      }

      cursor = dscFC.Search(null, true);
      feature = cursor.NextFeature();

      while (feature != null)
      {
        DscInfo dscInfo = new DscInfo();
        int dscId = (int)feature.get_Value(feature.Fields.FindField("dsc_id"));
        int toLinkIdSan = (int)feature.get_Value(feature.Fields.FindField("san_to_link_id"));
        dscInfo.Geometry = feature.ShapeCopy;
        dscInfo.ToLinkId = toLinkIdSan;
        dscInfo.ToLinkName = linkNames[toLinkIdSan];
        dscInfo.ConnectionFractionFromUsNode = (double)feature.get_Value(feature.Fields.FindField("conn_frac_from_link_us_node"));
        dscInfo.FloodReferenceElevation = (double)feature.get_Value(feature.Fields.FindField("flood_reference_elevation"));

        dscInfos.Add(dscId, dscInfo);
        feature = cursor.NextFeature();
      }

      return dscInfos;
    }

    private Dictionary<string, IGeometry> GetGeometryDictionary(IFeatureClass fc, string nameField)
    {
      Dictionary<string, IGeometry> geometries = new Dictionary<string, IGeometry>();

      IFeatureCursor cursor = fc.Search(null, true);
      IFeature feature = cursor.NextFeature();

      while (feature != null)
      {
        object o = feature.get_Value(feature.Fields.FindField(nameField));
        string featureName = o is string ? (string)o : o.ToString();
        geometries.Add(featureName, feature.ShapeCopy);

        feature = cursor.NextFeature();
      }
      return geometries;
    }
    private Dictionary<string, object> GetFieldDictionary(IFeatureClass fc, string nameField, string valueField)
    {
      Dictionary<string, object> values = new Dictionary<string, object>();

      IFeatureCursor cursor = fc.Search(null, true);
      IFeature feature = cursor.NextFeature();

      while (feature != null)
      {
        string featureName = feature.get_Value(feature.Fields.FindField(nameField)).ToString();
        object o = feature.get_Value(feature.Fields.FindField(valueField));
        values.Add(featureName, o);

        feature = cursor.NextFeature();
      }
      return values;
    }
    
    public ModelResultsSet LoadResults(string file)
    {
      ModelResultsSet modelResults = new ModelResultsSet();

      if (!System.IO.File.Exists(file))
      {
        throw new ArgumentException("File " + file + " not found");
      }

      try
      {
        IWorkspaceFactory workspaceFactory = new AccessWorkspaceFactoryClass();
        IFeatureWorkspace fws = (IFeatureWorkspace)workspaceFactory.OpenFromFile(file, 0);

        ReadModelResultsTable(modelResults, fws);
        ReadLinkResultsTable(modelResults, fws);
        ReadNodeResultsTable(modelResults, fws);

      }
      catch (Exception ex)
      {
        return null;
      }

      return modelResults;
    }
    
    private void ReadModelResultsTable(ModelResultsSet modelResults, IFeatureWorkspace fws)
    {
      IRow row = null;
      try
      {
        ITable modelResultsTable = (ITable)fws.OpenTable("MODEL_RESULTS");
        ICursor cursor = modelResultsTable.Search(null, true);
        row = cursor.NextRow();

        modelResults.ResultsFile = (string)row.get_Value(row.Fields.FindField("results_file"));
        modelResults.ModelEngine = (string)row.get_Value(row.Fields.FindField("model_engine"));
        modelResults.Name = (string)row.get_Value(row.Fields.FindField("name"));
        modelResults.Description = (string)row.get_Value(row.Fields.FindField("description"));
        modelResults.SimulationDateTime = (DateTime)row.get_Value(row.Fields.FindField("simulation_date"));

        cursor.Flush();
      }
      finally
      {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(row);
      }
    }
    private void ReadLinkResultsTable(ModelResultsSet modelResults, IFeatureWorkspace fws)
    {
      IRow row = null;
      LinkResult linkResult = null;

      ITable linkResultsTable = (ITable)fws.OpenTable("LINK_RESULTS");
      ICursor cursor = linkResultsTable.Search(null, true);
      row = cursor.NextRow();

      while (row != null)
      {
        linkResult = new LinkResult();

        linkResult.LinkName = (string)row.get_Value(row.Fields.FindField("modeled_link_name"));
        linkResult.MaxFlow = (double)row.get_Value(row.Fields.FindField("max_flow"));
        linkResult.MaxVelocity = (double)row.get_Value(row.Fields.FindField("max_velocity"));
        linkResult.MaxUsWaterElev = (double)row.get_Value(row.Fields.FindField("max_us_water_elev"));
        linkResult.MaxDsWaterElev = (double)row.get_Value(row.Fields.FindField("max_ds_water_elev"));
        linkResult.MaxQqRatio = (double)row.get_Value(row.Fields.FindField("max_qq_ratio"));
        linkResult.MaxDdRatio = (double)row.get_Value(row.Fields.FindField("max_dd_ratio"));

        modelResults.LinkResults.Add(linkResult.LinkName, linkResult);
        row = cursor.NextRow();
      }

      cursor.Flush();
    }
    private void ReadNodeResultsTable(ModelResultsSet modelResults, IFeatureWorkspace fws)
    {
      IRow row = null;
      NodeResult nodeResult = null;

      ITable nodeResultsTable = (ITable)fws.OpenTable("NODE_RESULTS");
      ICursor cursor = nodeResultsTable.Search(null, true);
      row = cursor.NextRow();

      while (row != null)
      {
        nodeResult = new NodeResult();

        nodeResult.NodeName = (string)row.get_Value(row.Fields.FindField("modeled_node_name"));
        nodeResult.MaxWaterElev = (double)row.get_Value(row.Fields.FindField("max_water_elev"));
        nodeResult.MaxSurcharge = (double)row.get_Value(row.Fields.FindField("max_surcharge"));
        nodeResult.MinFreeboard = (double)row.get_Value(row.Fields.FindField("min_freeboard"));
        nodeResult.SurchargedTime = (double)row.get_Value(row.Fields.FindField("surcharged_time"));
        nodeResult.FloodedTime = (double)row.get_Value(row.Fields.FindField("flooded_time"));        

        modelResults.NodeResults.Add(nodeResult.NodeName, nodeResult);
        row = cursor.NextRow();
      }

      cursor.Flush();
    }

    internal struct DscInfo
    {
      internal int DscId;
      internal int? ToLinkId;
      internal string ToLinkName;
      internal double ConnectionFractionFromUsNode;
      internal IGeometry Geometry;
      internal double FloodReferenceElevation;
    }
  }
}
